Tuya - Add smart meter support and fixture-based device test harness - PR5#2470
Tuya - Add smart meter support and fixture-based device test harness - PR5#2470Terdious wants to merge 90 commits intoGladysAssistant:masterfrom
Conversation
…er models - Added support for air conditioning devices with new mappings and DPS configurations. - Introduced local polling for Tuya devices to improve responsiveness. - Enhanced device conversion logic to include additional parameters such as cloud IP and local override. - Updated feature conversion to utilize advanced DPS mappings for air conditioning. - Implemented new models for air conditioning and power meter, including specific feature mappings. - Improved error handling and logging for local polling and device value setting. - Added unit tests for new feature mappings and conversion logic.
…y device conversion logic
…ttre à jour les tests associés
… champ d'erreur dans le payload de l'événement WebSocket
…s et ajouter des tests pour la gestion des appareils locaux
…on Tuya dans les fichiers de langue
…ion Tuya et mettre à jour les traductions
… des appareils Tuya
…tre l'utilisation d'adresses spécifiques
…eurs de port et mise à jour des traductions
…re des liens vers la documentation et les options de connexion
…réation de rapports GitHub pour les appareils Tuya
…l disconnect features - Added new translations for connection status messages in German, English, and French. - Implemented API endpoints to get Tuya connection status and to manually disconnect from the Tuya cloud. - Updated the Tuya service to handle automatic reconnection logic and manual disconnect state. - Enhanced the SetupTab component to reflect connection status and provide a disconnect button. - Added tests for the new functionality, including status retrieval and manual disconnect.
- Implemented device ranking and sorting in DiscoverTab for better user experience. - Added loading indicators and improved UI feedback during device scanning. - Refactored local polling logic to update discovered devices with local information. - Introduced utility functions for managing device parameters, including upserting and normalizing values. - Enhanced local scan response handling to merge existing device parameters. - Updated tests to cover new functionality and ensure reliability of device management.
…ls et des tests associés
… débogage pour la gestion des appareils
…age, ajouter des tests pour la reconnexion automatique et la découverte des appareils
… des paramètres dans le code de configuration Tuya
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (3)
front/src/routes/integration/all/tuya/TuyaDeviceBox.jsx (2)
554-641:⚠️ Potential issue | 🟠 MajorPreserve server-returned params after local poll.
When
response.deviceis returned with updated params, the code still rebuildsnewParamsfrom the pre-pollparamsarray (Line 607), which can drop fresh server-side params that may have been updated during the poll operation.,
💡 Suggested fix
- const newParams = [...params]; + const baseDevice = latestDevice || currentDevice; + const newParams = Array.isArray(baseDevice.params) ? [...baseDevice.params] : [...params]; if (usedProtocol) { const protocolIndex = newParams.findIndex(param => param.name === 'PROTOCOL_VERSION'); if (protocolIndex >= 0) { newParams[protocolIndex] = { ...newParams[protocolIndex], value: usedProtocol }; } else { newParams.push({ name: 'PROTOCOL_VERSION', value: usedProtocol }); } } - const baseDevice = latestDevice || currentDevice; this.setState({ device: { ...baseDevice, params: newParams },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@front/src/routes/integration/all/tuya/TuyaDeviceBox.jsx` around lines 554 - 641, The pollLocal handler rebuilds newParams from the pre-poll params array, which discards any updated params returned in response.device; change the logic to start newParams from latestDevice.params if available (fallback to current params) and then apply the PROTOCOL_VERSION update/insert as before, and ensure the setState call uses that merged params array when updating device; look for symbols pollLocal, params, response.device/updatedDevice/latestDevice, newParams and the final setState that writes device.params to implement this merge.
129-160:⚠️ Potential issue | 🟡 MinorUnknown-spec detection may over-report unsupported features.
This logic relies on exact code matching; Tuya payloads often expose equivalent data using different code names across sections, which can trigger false "partial support" and unnecessary issue creation prompts.
Based on learnings: In the Tuya integration for the Gladys Assistant repository, test fixtures preserve real-world inconsistencies (e.g., codes differing across specifications/status/properties sections) to ensure robust coverage of varying API payloads.
,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@front/src/routes/integration/all/tuya/TuyaDeviceBox.jsx` around lines 129 - 160, getUnknownSpecificationCodes currently compares codes by exact lowercased strings which over-reports differences when Tuya uses variant naming; in getUnknownSpecificationCodes normalize both knownCodes and specCodes more robustly by: when building knownCodes from features (and specCodes from specifications) trim and toLowerCase, also add canonical variants (strip common separators like '_' and '-', remove numeric instance suffixes like trailing digits or "_1"/"-1", and include the base token before any namespace delimiter ':'), then compare these canonical forms against getIgnoredCloudCodes canonicalized the same way; update references to knownCodes, specCodes and the final filter to use the canonical form so logically equivalent codes (e.g., "switch_1" vs "switch") are treated as matches rather than unknowns.front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx (1)
292-307:⚠️ Potential issue | 🟡 Minor
tuyaConfiguredderivation should mirror trimmed save inputs.
updateConfigurationmarks config as complete with whitespace-only values, while save-time logic trims and treats them as missing. This creates inconsistent UI state where "Save" button may be enabled but save will treat fields as empty.,
💡 Suggested fix
updateConfiguration = e => { const { name, value } = e.target; this.setState(prevState => { const nextState = { ...prevState, [name]: value }; - const configured = !!( - nextState.tuyaEndpoint && - nextState.tuyaAccessKey && - nextState.tuyaSecretKey && - nextState.tuyaAppAccountId - ); + const isFilled = key => String(nextState[key] || '').trim().length > 0; + const configured = + isFilled('tuyaEndpoint') && + isFilled('tuyaAccessKey') && + isFilled('tuyaSecretKey') && + isFilled('tuyaAppAccountId'); return { [name]: value, tuyaConfigured: configured }; }); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx` around lines 292 - 307, The updateConfiguration handler computes tuyaConfigured from raw values allowing whitespace-only strings to count as configured; change its derivation to use trimmed values so it mirrors save-time logic: in updateConfiguration (method name) build nextState as you do, but when computing configured check (nextState.tuyaEndpoint || '').trim(), (nextState.tuyaAccessKey || '').trim(), (nextState.tuyaSecretKey || '').trim(), and (nextState.tuyaAppAccountId || '').trim() instead of the raw properties; still persist the original value into state under [name], but set tuyaConfigured based on those trimmed checks so the Save button state matches save-time validation.
🧹 Nitpick comments (4)
server/test/services/tuya/lib/utils/tuya.deviceParams.test.js (1)
91-98: Addparamsassertions in partial/apply-existing scenarios to fully enforce sync contract.These cases currently assert top-level fields but not the mirrored
device.paramsentries. Adding those checks would better protect against regressions where field updates and param updates diverge.Suggested assertion additions
it('should keep protocol and product key when local info is partial', () => { const device = { ip: 'old', protocol_version: '3.3', product_key: 'pkey', params: [] }; const localInfo = { ip: '2.2.2.2' }; const updated = updateDiscoveredDeviceWithLocalInfo(device, localInfo); + const ipParam = updated.params.find((param) => param.name === DEVICE_PARAM_NAME.IP_ADDRESS); + const protocolParam = updated.params.find((param) => param.name === DEVICE_PARAM_NAME.PROTOCOL_VERSION); + const productKeyParam = updated.params.find((param) => param.name === DEVICE_PARAM_NAME.PRODUCT_KEY); expect(updated.ip).to.equal('2.2.2.2'); expect(updated.protocol_version).to.equal('3.3'); expect(updated.product_key).to.equal('pkey'); + expect(ipParam.value).to.equal('2.2.2.2'); + expect(protocolParam.value).to.equal('3.3'); + expect(productKeyParam.value).to.equal('pkey'); }); it('should apply existing local params', () => { const device = { ip: 'old', protocol_version: '3.1', local_override: false, params: [] }; @@ const updated = applyExistingLocalParams(device, existingDevice); + const ipParam = updated.params.find((param) => param.name === DEVICE_PARAM_NAME.IP_ADDRESS); + const protocolParam = updated.params.find((param) => param.name === DEVICE_PARAM_NAME.PROTOCOL_VERSION); + const overrideParam = updated.params.find((param) => param.name === DEVICE_PARAM_NAME.LOCAL_OVERRIDE); expect(updated.ip).to.equal('2.2.2.2'); expect(updated.protocol_version).to.equal('3.3'); expect(updated.local_override).to.equal(true); + expect(ipParam.value).to.equal('2.2.2.2'); + expect(protocolParam.value).to.equal('3.3'); + expect(overrideParam.value).to.equal(true); });Also applies to: 111-124
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/test/services/tuya/lib/utils/tuya.deviceParams.test.js` around lines 91 - 98, The test is missing assertions for device.params when local info is partial; update the two tests that check "partial/apply-existing" behavior (the one using updateDiscoveredDeviceWithLocalInfo around the lines shown and the similar case at lines ~111-124) to also assert that corresponding entries in updated.params reflect the top-level fields (ip, protocol_version, product_key) remain/are updated accordingly, by locating updateDiscoveredDeviceWithLocalInfo in the test and adding assertions that verify updated.params contains entries with the expected ip, protocol_version and product_key values.front/src/routes/integration/all/tuya/TuyaDeviceBox.jsx (2)
694-699: Popup manipulation may cause issues in some browsers.Setting
popup.opener = nulland manipulatingpopup.documentbefore navigation can fail silently if the popup is blocked or if cross-origin restrictions apply. The code handles the!popupcase but doesn't handle partial failures.💡 Consider wrapping popup manipulation in try-catch
const popup = window.open('about:blank', '_blank'); if (popup) { - popup.opener = null; - popup.document.title = 'GitHub'; - popup.document.body.innerText = 'Searching for existing issues...'; + try { + popup.opener = null; + popup.document.title = 'GitHub'; + popup.document.body.innerText = 'Searching for existing issues...'; + } catch (e) { + // Some browsers may restrict document access + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@front/src/routes/integration/all/tuya/TuyaDeviceBox.jsx` around lines 694 - 699, The popup creation and direct DOM access (window.open -> popup, then popup.opener and popup.document.*) can fail silently due to blockers or cross-origin limits; wrap the popup manipulation and document writes in a try-catch around the block that sets popup.opener, popup.document.title, and popup.document.body.innerText (the code that runs after const popup = window.open(...)), check that popup still exists and is not closed before touching it, and if an exception or partial failure occurs fall back to safe behavior such as navigating the popup via popup.location.href to the target URL or opening the target in the current window/tab; ensure you reference and protect the popup variable and the section that assigns popup.opener and manipulates popup.document.
287-315: Consider surfacing GitHub API failures to the user.When the GitHub search fails (Line 309), the error is thrown but may not always surface clearly in the UI. Users may be confused if the issue check silently fails and the flow proceeds with
shouldOpenIssue = true.💡 Consider adding user feedback for search failures
} catch (error) { shouldOpenIssue = true; + // Optional: surface this to user so they know the duplicate check couldn't complete + console.warn('GitHub issue search failed, proceeding with issue creation', error); } finally { this.setState({ githubIssueChecking: false }); }Alternatively, you could set a state flag like
githubIssueCheckFailedand show a brief warning in the UI.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@front/src/routes/integration/all/tuya/TuyaDeviceBox.jsx` around lines 287 - 315, The GitHub search error is currently thrown inside checkGithubIssueExists which can leave the UI unaware; modify checkGithubIssueExists to catch fetch/response/json errors (around the fetch/response.ok/json sections), log the error, set/update a component state flag (e.g. githubIssueCheckFailed via a setter you add) and return a sentinel (null/undefined) instead of throwing; then update the caller logic that computes shouldOpenIssue to treat a null result as "unknown" (don’t auto-open an issue) and render a brief UI warning when githubIssueCheckFailed is true so users see the failure; keep existing cache behavior (githubIssueCache and setGithubIssueCache) for successful responses.server/test/services/tuya/lib/tuya.localScan.test.js (1)
8-31: Consider extracting a shared socket stub factory to reduce duplication.The dgram/socket scaffolding is repeated across tests with small variations. A helper would reduce maintenance cost and make new scenarios easier to add safely.
Also applies to: 82-99, 150-165, 199-228, 255-275
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/test/services/tuya/lib/tuya.localScan.test.js` around lines 8 - 31, Extract the repeated socket scaffolding into a shared factory (e.g., createSocketStub / makeDgramStub) and replace the inline dgramStub definitions with a call to that factory; the factory should return an object with createSocket that pushes sockets into a provided sockets array and returns a socket exposing on, bind (supporting both cb and options-as-cb), close, and handlers properties to match the current tests that reference dgramStub.createSocket, sockets, and socket.handlers. Update all duplicated occurrences (the blocks around lines 82-99, 150-165, 199-228, 255-275) to use the new factory so tests reuse the same stub shape and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx`:
- Around line 356-413: Multiple alert blocks can render at the same time (e.g.,
state.tuyaSaveSettingsStatus === RequestStatus.Error and
state.tuyaConnectionStatus === RequestStatus.Error), so make the alerts mutually
exclusive by adding precedence checks or converting the separate JSX blocks into
a single conditional chain; for example, render the save-settings error block
only when state.tuyaSaveSettingsStatus === RequestStatus.Error, else-if render
the connection error block when state.tuyaConnectionStatus ===
RequestStatus.Error, and similarly ensure other alerts check that
higher-priority states (state.tuyaSaveSettingsStatus,
state.tuyaConnectionStatus, state.tuyaConnecting, state.tuyaConnected,
state.tuyaManuallyDisconnected) are not active before rendering; update the JSX
around the blocks referencing RequestStatus.Error, state.tuyaSaveSettingsStatus,
state.tuyaConnectionStatus, state.tuyaConnectionError and
this.renderTuyaError(...) so only one alert is shown at a time (or centralize
logic in a helper like getActiveTuyaAlert() used by the render).
In `@server/services/tuya/lib/mappings/index.js`:
- Around line 180-184: extractCodesFromProperties currently only handles
payloads shaped as { properties: [] } and ignores the case where
propertiesPayload is itself an array; update the logic that computes the local
variable properties so it accepts either an array payload or an
object-with-properties payload (i.e., if Array.isArray(propertiesPayload) use
propertiesPayload, else if propertiesPayload &&
Array.isArray(propertiesPayload.properties) use propertiesPayload.properties,
otherwise []). Adjust any downstream code in extractCodesFromProperties that
iterates over properties so it works with the unified array.
In `@server/services/tuya/lib/tuya.poll.js`:
- Around line 292-295: The final cloud call to pollCloudFeatures in poll is
unguarded and can reject the whole poll; wrap the pollCloudFeatures.call(this,
deviceFeatures, topic) invocation in a try/catch inside the same code path (same
function/poll) so transient cloud errors are caught, set a safe default
cloudSummary (e.g. {handled:0, changed:0, missing:0}) or reuse a previous
summary, update fallbackReason and log the error via logger.error, then continue
to the logger.debug line so scheduled polling is not interrupted by cloud
failures.
In `@server/test/services/tuya/lib/tuya.localScan.test.js`:
- Around line 253-293: The test currently only ensures localScan(1) resolves;
modify it to capture and assert the resolved value: await the promise returned
by localScan(1) into a variable (e.g., result) after clock.tickAsync, then add
assertions that result is an array and contains the expected device entry coming
from our stubs — e.g., an object with payload null and the socket address/port
from dgramStub (address '0.0.0.0', port 6666); reference symbols: localScan,
dgramStub/sockets, MessageParserStub. Ensure the assertions run before
clock.restore().
---
Duplicate comments:
In `@front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx`:
- Around line 292-307: The updateConfiguration handler computes tuyaConfigured
from raw values allowing whitespace-only strings to count as configured; change
its derivation to use trimmed values so it mirrors save-time logic: in
updateConfiguration (method name) build nextState as you do, but when computing
configured check (nextState.tuyaEndpoint || '').trim(), (nextState.tuyaAccessKey
|| '').trim(), (nextState.tuyaSecretKey || '').trim(), and
(nextState.tuyaAppAccountId || '').trim() instead of the raw properties; still
persist the original value into state under [name], but set tuyaConfigured based
on those trimmed checks so the Save button state matches save-time validation.
In `@front/src/routes/integration/all/tuya/TuyaDeviceBox.jsx`:
- Around line 554-641: The pollLocal handler rebuilds newParams from the
pre-poll params array, which discards any updated params returned in
response.device; change the logic to start newParams from latestDevice.params if
available (fallback to current params) and then apply the PROTOCOL_VERSION
update/insert as before, and ensure the setState call uses that merged params
array when updating device; look for symbols pollLocal, params,
response.device/updatedDevice/latestDevice, newParams and the final setState
that writes device.params to implement this merge.
- Around line 129-160: getUnknownSpecificationCodes currently compares codes by
exact lowercased strings which over-reports differences when Tuya uses variant
naming; in getUnknownSpecificationCodes normalize both knownCodes and specCodes
more robustly by: when building knownCodes from features (and specCodes from
specifications) trim and toLowerCase, also add canonical variants (strip common
separators like '_' and '-', remove numeric instance suffixes like trailing
digits or "_1"/"-1", and include the base token before any namespace delimiter
':'), then compare these canonical forms against getIgnoredCloudCodes
canonicalized the same way; update references to knownCodes, specCodes and the
final filter to use the canonical form so logically equivalent codes (e.g.,
"switch_1" vs "switch") are treated as matches rather than unknowns.
---
Nitpick comments:
In `@front/src/routes/integration/all/tuya/TuyaDeviceBox.jsx`:
- Around line 694-699: The popup creation and direct DOM access (window.open ->
popup, then popup.opener and popup.document.*) can fail silently due to blockers
or cross-origin limits; wrap the popup manipulation and document writes in a
try-catch around the block that sets popup.opener, popup.document.title, and
popup.document.body.innerText (the code that runs after const popup =
window.open(...)), check that popup still exists and is not closed before
touching it, and if an exception or partial failure occurs fall back to safe
behavior such as navigating the popup via popup.location.href to the target URL
or opening the target in the current window/tab; ensure you reference and
protect the popup variable and the section that assigns popup.opener and
manipulates popup.document.
- Around line 287-315: The GitHub search error is currently thrown inside
checkGithubIssueExists which can leave the UI unaware; modify
checkGithubIssueExists to catch fetch/response/json errors (around the
fetch/response.ok/json sections), log the error, set/update a component state
flag (e.g. githubIssueCheckFailed via a setter you add) and return a sentinel
(null/undefined) instead of throwing; then update the caller logic that computes
shouldOpenIssue to treat a null result as "unknown" (don’t auto-open an issue)
and render a brief UI warning when githubIssueCheckFailed is true so users see
the failure; keep existing cache behavior (githubIssueCache and
setGithubIssueCache) for successful responses.
In `@server/test/services/tuya/lib/tuya.localScan.test.js`:
- Around line 8-31: Extract the repeated socket scaffolding into a shared
factory (e.g., createSocketStub / makeDgramStub) and replace the inline
dgramStub definitions with a call to that factory; the factory should return an
object with createSocket that pushes sockets into a provided sockets array and
returns a socket exposing on, bind (supporting both cb and options-as-cb),
close, and handlers properties to match the current tests that reference
dgramStub.createSocket, sockets, and socket.handlers. Update all duplicated
occurrences (the blocks around lines 82-99, 150-165, 199-228, 255-275) to use
the new factory so tests reuse the same stub shape and behavior.
In `@server/test/services/tuya/lib/utils/tuya.deviceParams.test.js`:
- Around line 91-98: The test is missing assertions for device.params when local
info is partial; update the two tests that check "partial/apply-existing"
behavior (the one using updateDiscoveredDeviceWithLocalInfo around the lines
shown and the similar case at lines ~111-124) to also assert that corresponding
entries in updated.params reflect the top-level fields (ip, protocol_version,
product_key) remain/are updated accordingly, by locating
updateDiscoveredDeviceWithLocalInfo in the test and adding assertions that
verify updated.params contains entries with the expected ip, protocol_version
and product_key values.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
front/src/routes/integration/all/tuya/TuyaDeviceBox.jsxfront/src/routes/integration/all/tuya/setup-page/SetupTab.jsxserver/services/tuya/lib/mappings/index.jsserver/services/tuya/lib/tuya.poll.jsserver/test/services/tuya/lib/mappings/index.test.jsserver/test/services/tuya/lib/tuya.localPoll.test.jsserver/test/services/tuya/lib/tuya.localScan.test.jsserver/test/services/tuya/lib/tuya.poll.test.jsserver/test/services/tuya/lib/utils/tuya.deviceParams.test.js
🚧 Files skipped from review as they are similar to previous changes (2)
- server/test/services/tuya/lib/mappings/index.test.js
- server/test/services/tuya/lib/tuya.localPoll.test.js
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
server/test/services/tuya/lib/device/tuya.localMapping.fixtures.test.js (1)
20-23:⚠️ Potential issue | 🟡 MinorEnforce the expected
external_idshape before extracting code.A value like
"foo"still passes; asserting presence of:will fail fast on malformed fixtures instead of silently accepting them.✅ Suggested tightening
const resolvedMapping = currentDevice.features.reduce((accumulator, feature) => { expect(feature.external_id).to.be.a('string'); - const code = String(feature.external_id) + expect(feature.external_id).to.include(':'); + const code = feature.external_id .split(':') .pop();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/test/services/tuya/lib/device/tuya.localMapping.fixtures.test.js` around lines 20 - 23, The test currently extracts `code` by splitting `feature.external_id` without asserting its expected shape; add an assertion that `feature.external_id` contains a colon (e.g., expect(feature.external_id).to.include(':') or match a /:/ regex) before performing the String(...).split(':').pop() extraction so malformed values like "foo" fail fast; update the assertion around `feature.external_id` in the tuya.localMapping.fixtures.test to verify the presence of ':' then proceed to extract `code`.
🧹 Nitpick comments (4)
server/services/tuya/lib/mappings/index.js (1)
70-88: Consider the match order behavior.The
matchDeviceTypefunction returnstrueearly when category or productId matches, before checking codes and keywords. This is likely intentional for performance, but be aware thatObject.values(DEVICE_TYPE_INDEX).find()at line 227-228 will return the first matching type in iteration order.Since
LIST_DEVICE_TYPES = [SMART_SOCKET, SMART_METER]defines the order, SMART_SOCKET will be checked first. If a device matches multiple type definitions, the first match wins. This should be fine for the current device types but is worth noting if more types are added later.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/services/tuya/lib/mappings/index.js` around lines 70 - 88, The current matchDeviceType function returns true immediately when category or productId matches, which lets Object.values(DEVICE_TYPE_INDEX).find(...) and LIST_DEVICE_TYPES ordering determine which type wins; to avoid accidental misclassification make matching stricter: update matchDeviceType to not short-circuit on category/productId alone but instead treat those as positive signals combined with the REQUIRED_CODES and KEYWORDS checks (i.e., evaluate requiredCodes/modelName/keywords regardless of category/productId and only return true when both the category/productId signal and the code/keyword checks pass), or alternatively add a clear comment near matchDeviceType and the LIST_DEVICE_TYPES/DEVICE_TYPE_INDEX usage documenting that the first match wins and that LIST_DEVICE_TYPES defines priority so future developers are aware; reference matchDeviceType, DEVICE_TYPE_INDEX, and LIST_DEVICE_TYPES when making the change.server/services/tuya/lib/tuya.poll.js (1)
194-221: Clarify local-config semantics to avoid misleading diagnostics.
hasLocalConfigcurrently mixes “credentials exist” with “local mode enabled”. This makeshas_locallogs harder to reason about and couples two concerns in branch conditions. Split intohasLocalCredentialsandshouldUseLocal.♻️ Proposed refactor
- const hasLocalConfig = Boolean(ipAddress && localKey && protocolVersion && localOverride === true); - const requestedMode = localOverride === true ? 'local' : 'cloud'; + const hasLocalCredentials = Boolean(ipAddress && localKey && protocolVersion); + const shouldUseLocal = localOverride === true; + const requestedMode = shouldUseLocal ? 'local' : 'cloud'; logger.debug( `[Tuya][poll] device=${topic} requested=${requestedMode} has_local=${Boolean( - hasLocalConfig, + hasLocalCredentials, )} protocol=${protocolVersion || 'none'} ip=${ipAddress || 'none'}`, ); @@ - if (localOverride === true && !hasLocalConfig) { + if (shouldUseLocal && !hasLocalCredentials) { @@ - if (hasLocalConfig) { + if (shouldUseLocal && hasLocalCredentials) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/services/tuya/lib/tuya.poll.js` around lines 194 - 221, hasLocalConfig conflates "credentials exist" with "local mode requested", causing confusing logs and branch logic; split it into hasLocalCredentials = Boolean(ipAddress && localKey && protocolVersion) and shouldUseLocal = localOverride === true, then update all uses: replace hasLocalConfig checks with hasLocalCredentials where you only care about config presence, and use shouldUseLocal where you care whether local mode was requested (e.g., computing requestedMode, fallback when shouldUseLocal && !hasLocalCredentials, and deciding to enter local handling under hasLocalCredentials && shouldUseLocal if appropriate); also change the logger.debug field from has_local to hasLocalCredentials (or both as needed) and rename localHandled/localChanged usage to reflect these clearer semantics (ensure fallbackReason logic uses shouldUseLocal and hasLocalCredentials).front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx (2)
560-569: Consider also disabling disconnect when already manually disconnected.The button could be clicked when
tuyaManuallyDisconnectedis already true, which would trigger a redundant API call.💡 Suggested enhancement
disabled={!state.tuyaConfigured || state.tuyaDisconnecting || state.tuyaConnecting} + // Also consider: || state.tuyaManuallyDisconnected🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx` around lines 560 - 569, The disconnect button can be clicked when tuyaManuallyDisconnected is already true, causing a redundant API call; update the disabled condition on the button to also check state.tuyaManuallyDisconnected and additionally add a guard at the start of the disconnect handler (this.disconnectFromCloud) to return early if this.state.tuyaManuallyDisconnected is true so the UI and handler both prevent duplicate disconnect attempts.
155-181: Sequential HTTP calls can be parallelized for better performance.These seven independent variable saves are executed sequentially. Parallelizing them with
Promise.allwould significantly reduce save latency.♻️ Suggested refactor
- await this.props.httpClient.post('/api/v1/service/tuya/variable/TUYA_ENDPOINT', { - value: tuyaEndpoint - }); - - await this.props.httpClient.post('/api/v1/service/tuya/variable/TUYA_ACCESS_KEY', { - value: tuyaAccessKey - }); - - await this.props.httpClient.post('/api/v1/service/tuya/variable/TUYA_SECRET_KEY', { - value: tuyaSecretKey - }); - - await this.props.httpClient.post('/api/v1/service/tuya/variable/TUYA_APP_ACCOUNT_UID', { - value: tuyaAppAccountId - }); - - await this.props.httpClient.post('/api/v1/service/tuya/variable/TUYA_APP_USERNAME', { - value: tuyaAppUsername - }); - - await this.props.httpClient.post('/api/v1/service/tuya/variable/TUYA_LAST_CONNECTED_CONFIG_HASH', { - value: '' - }); - - await this.props.httpClient.post('/api/v1/service/tuya/variable/TUYA_MANUAL_DISCONNECT', { - value: 'false' - }); + await Promise.all([ + this.props.httpClient.post('/api/v1/service/tuya/variable/TUYA_ENDPOINT', { value: tuyaEndpoint }), + this.props.httpClient.post('/api/v1/service/tuya/variable/TUYA_ACCESS_KEY', { value: tuyaAccessKey }), + this.props.httpClient.post('/api/v1/service/tuya/variable/TUYA_SECRET_KEY', { value: tuyaSecretKey }), + this.props.httpClient.post('/api/v1/service/tuya/variable/TUYA_APP_ACCOUNT_UID', { value: tuyaAppAccountId }), + this.props.httpClient.post('/api/v1/service/tuya/variable/TUYA_APP_USERNAME', { value: tuyaAppUsername }), + this.props.httpClient.post('/api/v1/service/tuya/variable/TUYA_LAST_CONNECTED_CONFIG_HASH', { value: '' }), + this.props.httpClient.post('/api/v1/service/tuya/variable/TUYA_MANUAL_DISCONNECT', { value: 'false' }) + ]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx` around lines 155 - 181, The seven independent this.props.httpClient.post calls in SetupTab.jsx are executed sequentially; replace them by creating an array of post promises (each calling this.props.httpClient.post with the same endpoints/values shown: TUYA_ENDPOINT, TUYA_ACCESS_KEY, TUYA_SECRET_KEY, TUYA_APP_ACCOUNT_UID, TUYA_APP_USERNAME, TUYA_LAST_CONNECTED_CONFIG_HASH (''), TUYA_MANUAL_DISCONNECT ('false')) and await them with Promise.all so the requests run in parallel; ensure you still await the Promise.all result and propagate or handle any errors the same way as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx`:
- Around line 127-131: The catch block in the SetupTab component currently
clears tuyaStatusLoading but hides the failure; update the catch in SetupTab.jsx
to record the error and expose it to the UI by calling this.setState with
tuyaStatusLoading: false plus an error flag/message (e.g., tuyaStatusError: true
and tuyaStatusErrorMessage: e.message or e.toString()), and also log the error
(console.error or the app's logger) so the UI can render an error indicator
instead of silently failing.
- Around line 511-518: The eye-toggle span in SetupTab.jsx (the element using
this.toggleClientSecret and state.showClientSecret) is not keyboard accessible;
update it to be reachable and operable via keyboard by either replacing the
<span> with a semantic <button> or adding role="button", tabIndex={0}, an
onKeyDown handler that calls this.toggleClientSecret when Enter or Space is
pressed, and an appropriate ARIA attribute (e.g., aria-pressed or aria-label
reflecting showClientSecret) so screen reader and keyboard users can activate
and understand the control.
In `@front/src/routes/integration/all/tuya/TuyaDeviceBox.jsx`:
- Around line 147-157: forEach callbacks in the services.forEach and
properties.forEach calls are using expression-bodied arrow functions which
implicitly return a value and trigger the lint rule; change both callbacks to
use block-bodied arrow functions (or plain function expressions) that call
addKnownCode(property && property.code) without returning anything (i.e., wrap
the call in { ... }), and ensure you handle potential nulls via the existing
property && property.code check; targets: the services.forEach callback and the
properties.forEach callback that invoke addKnownCode, and the
propertiesPayload/device handling that feeds the second properties array.
- Around line 806-834: openEmptyGithubIssue currently calls window.open after
awaiting checkGithubIssueExists, losing the user gesture; to fix, after the
initial synchronous guards (the early return that checks githubIssuePayloadUrl,
githubIssueChecking, githubIssueExists, githubIssueOpened, device) open a
lightweight placeholder window synchronously (e.g., const win = window.open('',
'_blank')) and keep the reference, then proceed to set githubIssueChecking and
await checkGithubIssueExists as before; if the check determines not to open,
close the placeholder (win.close()), otherwise build the encoded title with
buildIssueTitle and navigate the placeholder to the final URL (win.location.href
= `${GITHUB_BASE_URL}?title=${title}`), and finally update githubIssueOpened
state — ensure you still set githubIssueChecking state around the async call and
handle errors by navigating the placeholder or closing it appropriately.
In `@server/services/tuya/lib/tuya.localScan.js`:
- Around line 85-90: The current assignment for devices[deviceId] replaces any
existing entry with possibly-sparse values; instead merge new values into the
existing object so earlier non-undefined fields are preserved: when handling
deviceId in tuya.localScan.js, read the previous entry (devices[deviceId] or
{}), and set ip, version, productKey only when the incoming
resolvedIp/version/productKey are defined (or prefer the new value otherwise),
then assign the merged object back to devices[deviceId]; keep the isNew
detection logic (const isNew = !devices[deviceId]) but perform the merge
afterwards so duplicate sparse packets do not wipe richer prior data.
---
Duplicate comments:
In `@server/test/services/tuya/lib/device/tuya.localMapping.fixtures.test.js`:
- Around line 20-23: The test currently extracts `code` by splitting
`feature.external_id` without asserting its expected shape; add an assertion
that `feature.external_id` contains a colon (e.g.,
expect(feature.external_id).to.include(':') or match a /:/ regex) before
performing the String(...).split(':').pop() extraction so malformed values like
"foo" fail fast; update the assertion around `feature.external_id` in the
tuya.localMapping.fixtures.test to verify the presence of ':' then proceed to
extract `code`.
---
Nitpick comments:
In `@front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx`:
- Around line 560-569: The disconnect button can be clicked when
tuyaManuallyDisconnected is already true, causing a redundant API call; update
the disabled condition on the button to also check
state.tuyaManuallyDisconnected and additionally add a guard at the start of the
disconnect handler (this.disconnectFromCloud) to return early if
this.state.tuyaManuallyDisconnected is true so the UI and handler both prevent
duplicate disconnect attempts.
- Around line 155-181: The seven independent this.props.httpClient.post calls in
SetupTab.jsx are executed sequentially; replace them by creating an array of
post promises (each calling this.props.httpClient.post with the same
endpoints/values shown: TUYA_ENDPOINT, TUYA_ACCESS_KEY, TUYA_SECRET_KEY,
TUYA_APP_ACCOUNT_UID, TUYA_APP_USERNAME, TUYA_LAST_CONNECTED_CONFIG_HASH (''),
TUYA_MANUAL_DISCONNECT ('false')) and await them with Promise.all so the
requests run in parallel; ensure you still await the Promise.all result and
propagate or handle any errors the same way as before.
In `@server/services/tuya/lib/mappings/index.js`:
- Around line 70-88: The current matchDeviceType function returns true
immediately when category or productId matches, which lets
Object.values(DEVICE_TYPE_INDEX).find(...) and LIST_DEVICE_TYPES ordering
determine which type wins; to avoid accidental misclassification make matching
stricter: update matchDeviceType to not short-circuit on category/productId
alone but instead treat those as positive signals combined with the
REQUIRED_CODES and KEYWORDS checks (i.e., evaluate
requiredCodes/modelName/keywords regardless of category/productId and only
return true when both the category/productId signal and the code/keyword checks
pass), or alternatively add a clear comment near matchDeviceType and the
LIST_DEVICE_TYPES/DEVICE_TYPE_INDEX usage documenting that the first match wins
and that LIST_DEVICE_TYPES defines priority so future developers are aware;
reference matchDeviceType, DEVICE_TYPE_INDEX, and LIST_DEVICE_TYPES when making
the change.
In `@server/services/tuya/lib/tuya.poll.js`:
- Around line 194-221: hasLocalConfig conflates "credentials exist" with "local
mode requested", causing confusing logs and branch logic; split it into
hasLocalCredentials = Boolean(ipAddress && localKey && protocolVersion) and
shouldUseLocal = localOverride === true, then update all uses: replace
hasLocalConfig checks with hasLocalCredentials where you only care about config
presence, and use shouldUseLocal where you care whether local mode was requested
(e.g., computing requestedMode, fallback when shouldUseLocal &&
!hasLocalCredentials, and deciding to enter local handling under
hasLocalCredentials && shouldUseLocal if appropriate); also change the
logger.debug field from has_local to hasLocalCredentials (or both as needed) and
rename localHandled/localChanged usage to reflect these clearer semantics
(ensure fallbackReason logic uses shouldUseLocal and hasLocalCredentials).
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (22)
front/src/config/i18n/en.jsonfront/src/config/i18n/fr.jsonfront/src/routes/integration/all/tuya/TuyaDeviceBox.jsxfront/src/routes/integration/all/tuya/setup-page/SetupTab.jsxserver/services/tuya/lib/mappings/index.jsserver/services/tuya/lib/tuya.loadDevices.jsserver/services/tuya/lib/tuya.localPoll.jsserver/services/tuya/lib/tuya.localScan.jsserver/services/tuya/lib/tuya.manualDisconnect.jsserver/services/tuya/lib/tuya.poll.jsserver/test/services/tuya/fixtures/fixtureHelper.jsserver/test/services/tuya/lib/device/feature/tuya.deviceMapping.test.jsserver/test/services/tuya/lib/device/tuya.convertDevice.fixtures.test.jsserver/test/services/tuya/lib/device/tuya.localMapping.fixtures.test.jsserver/test/services/tuya/lib/mappings/index.test.jsserver/test/services/tuya/lib/tuya.disconnect.test.jsserver/test/services/tuya/lib/tuya.discoverDevices.test.jsserver/test/services/tuya/lib/tuya.loadDevices.test.jsserver/test/services/tuya/lib/tuya.localPoll.test.jsserver/test/services/tuya/lib/tuya.localScan.test.jsserver/test/services/tuya/lib/tuya.manualDisconnect.test.jsserver/test/services/tuya/lib/tuya.poll.test.js
✅ Files skipped from review due to trivial changes (1)
- server/test/services/tuya/lib/tuya.localScan.test.js
🚧 Files skipped from review as they are similar to previous changes (4)
- server/test/services/tuya/lib/tuya.disconnect.test.js
- server/test/services/tuya/lib/device/tuya.convertDevice.fixtures.test.js
- server/test/services/tuya/lib/tuya.discoverDevices.test.js
- server/test/services/tuya/lib/tuya.manualDisconnect.test.js
There was a problem hiding this comment.
♻️ Duplicate comments (1)
front/src/routes/integration/all/tuya/TuyaDeviceBox.jsx (1)
814-842:⚠️ Potential issue | 🟠 Major
openEmptyGithubIssuestill opens popup after async work and will be blocked by browsers.Line 840 calls
window.openafterawait checkGithubIssueExists(...)on line 826. Browsers enforce transient user activation and lose the user gesture once anawaitis hit, causing popup blockers to treat this as non-user-initiated.Note:
handleCreateGithubIssuewas correctly fixed (opens popup at line 718 before any await), but this method was not.💡 Suggested fix: Open the window synchronously before the async work
openEmptyGithubIssue = async e => { if (e && e.preventDefault) { e.preventDefault(); } const { githubIssuePayloadUrl, githubIssueChecking, githubIssueExists, githubIssueOpened, device } = this.state; if (!githubIssuePayloadUrl || githubIssueChecking || githubIssueExists || githubIssueOpened || !device) { return; } const issueTitle = buildIssueTitle(device); + const popup = window.open('about:blank', '_blank'); + if (popup) { + popup.opener = null; + popup.document.title = 'GitHub'; + popup.document.body.innerText = 'Searching for existing issues...'; + } this.setState({ githubIssueChecking: true }); let shouldOpenIssue = true; try { const exists = await checkGithubIssueExists(issueTitle); if (exists) { shouldOpenIssue = false; this.setState({ githubIssueExists: true }); } } catch (error) { shouldOpenIssue = true; } finally { this.setState({ githubIssueChecking: false }); } if (!shouldOpenIssue) { + if (popup && !popup.closed) { + popup.close(); + } return; } const title = encodeURIComponent(issueTitle); - window.open(`${GITHUB_BASE_URL}?title=${title}`, '_blank'); + if (popup && !popup.closed) { + popup.location.href = `${GITHUB_BASE_URL}?title=${title}`; + } else { + window.open(`${GITHUB_BASE_URL}?title=${title}`, '_blank'); + } this.setState({ githubIssueOpened: true }); };
(cherry picked from commit e486f97)
Pull Request check-list
To ensure your Pull Request can be accepted as fast as possible, make sure to review and check all of these items:
npm teston both front/server)npm run eslinton both front/server)npm run prettieron both front/server)npm run compare-translationson front)front/src/config/demo.js) so that the demo website is working without a backend? (if needed) See https://demo.gladysassistant.com.NOTE: these things are not required to open a PR and can be done afterwards / while the PR is open.
Description of change
Summary
product_id: bbcg1hrkrj5rifsd).Details
/v1.0/iot-03/devices/{id}/status/v2.0/cloud/thing/{id}/shadow/propertiesspecifications.statusis empty and the device only exposes a Thing Model.Why this matters
This PR does more than add one device.
It also introduces a repeatable fixture-based approach for Tuya device integration:
It also closes an important gap for Tuya devices that are cloud-readable through the Thing Model shadow API but do not expose legacy cloud status specifications.
This makes onboarding future Tuya devices more industrialized, faster, and safer.
Scope
Compared to
tuya-local-github-issues:Front (prod)
+839 lines / -334 linesTranslations
+6 lines / -3 linesServer (prod)
+514 lines / -49 linesServer Tests
+2669 lines / -20 linesNo package changes.
Total
58 files changed+4028 lines / -406 linesSummary by CodeRabbit
New Features
Improvements
Tests